/* * ****************************************************************************** * MontiCore Language Workbench * Copyright (c) 2015, MontiCore, All rights reserved. * * This project is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3.0 of the License, or (at your option) any later version. * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this project. If not, see <http://www.gnu.org/licenses/>. * ****************************************************************************** */ package de.monticore.mojo; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import de.monticore.MontiCoreConfiguration; import de.monticore.MontiCoreScript; import de.se_rwth.commons.configuration.Configuration; import de.se_rwth.commons.configuration.ConfigurationPropertiesMapContributor; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.DependencyResolutionRequiredException; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.project.MavenProject; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.file.Path; import java.util.*; import static com.google.common.base.MoreObjects.firstNonNull; import static de.monticore.MontiCoreConfiguration.Options.*; /** * Invokes {@link MontiCore} using the given configuration parameters. * * @author (last commit) $Author$ * @version $Revision$, $Date$ */ @Mojo(name = "generate", defaultPhase = LifecyclePhase.GENERATE_SOURCES, requiresDependencyResolution = ResolutionScope.COMPILE) public final class GenerateMojo extends AbstractMojo { /** * The current Maven project. */ @Parameter(defaultValue = "${project}", readonly = true, required = true) private MavenProject mavenProject; /** * @return mavenProject */ protected MavenProject getMavenProject() { return this.mavenProject; } /** * The set of grammars/directories to be passed to MontiCore. This is the * "grammar" option of MontiCore and defaults to "src/main/grammars". */ @Parameter private List<File> grammars; /** * @return the value of the "grammars" configuration parameter. */ protected Set<File> getGrammars() { ImmutableSet.Builder<File> grammarFilesBuilder = ImmutableSet.builder(); if (this.grammars != null) { this.grammars.forEach(g -> grammarFilesBuilder.add(fromBasePath(g))); } ImmutableSet<File> grammarFiles = grammarFilesBuilder.build(); return grammarFiles.isEmpty() ? ImmutableSet.of(getDefaultGrammarDirectory()) : grammarFiles; } /** * @return the default directory for MontiCore grammars. */ public File getDefaultGrammarDirectory() { return fromBasePath("src/main/grammars"); } /** * The output directory for source code generated by MontiCore. This is the * "outputDirectory" option of MontiCore. */ @Parameter(defaultValue = "${project.build.directory}/generated-sources/${plugin.goalPrefix}/sourcecode") private File outputDirectory; /** * @return the value of the "outputDirectory" configuration parameter. */ protected File getOutputDirectory() { return fromBasePath(this.outputDirectory); } /** * The output directory for symbol tables generated by MontiCore. This is the * "symbolTableDirectory" option of MontiCore. */ @Parameter(defaultValue = "${project.build.directory}/generated-sources/${plugin.goalPrefix}/symboltable") private File symbolTableDirectory; /** * @return the value of the "symbolTableDirectory" configuration parameter. */ protected File getSymbolTableDirectory() { return fromBasePath(this.symbolTableDirectory); } /** * The set of directories containing handwritten code to be integrated into * generated code. This is the "handcodedPath" option of MontiCore and * defaults to "src/main/java". */ @Parameter private List<File> handcodedPaths; /** * @return the value of the "targetPaths" configuration parameter. */ protected Set<File> getHandcodedPaths() { ImmutableSet.Builder<File> handcodedPathsBuilder = ImmutableSet.builder(); if (this.handcodedPaths != null) { this.handcodedPaths.forEach(t -> handcodedPathsBuilder.add(fromBasePath(t))); } ImmutableSet<File> handcodedPathFiles = handcodedPathsBuilder.build(); return handcodedPathFiles.isEmpty() ? ImmutableSet.of(getDefaultHandcodedPath()) : handcodedPathFiles; } /** * @return the default path for handwritten code. */ public File getDefaultHandcodedPath() { return fromBasePath("src/main/java"); } /** * The set of directories containing templates to be integrated into the code * generation process. This is the "templatePath" option of MontiCore and * defaults to "src/main/resources". */ @Parameter private List<File> templatePaths; /** * @return the value of the "templatePaths" configuration parameter. */ protected Set<File> getTemplatePaths() { ImmutableSet.Builder<File> templatePathsBuilder = ImmutableSet.builder(); if (this.templatePaths != null) { this.templatePaths.forEach(t -> templatePathsBuilder.add(fromBasePath(t))); } ImmutableSet<File> templatePathFiles = templatePathsBuilder.build(); return templatePathFiles.isEmpty() ? getDefaultTemplatePath().map(ImmutableSet::of).orElse(ImmutableSet.of()) : templatePathFiles; } /** * @return the default path for templates. Returns empty if the directory doesn't exist. */ public Optional<File> getDefaultTemplatePath() { return Optional.of(fromBasePath("src/main/resources")).filter(File::exists); } /** * The set of models/directories to be passed to MontiCore as part of the * model path. By default all grammar directories as specified in the * "grammars" parameter as well as any matching project dependency artifacts * as specified by the parameters "modelPathDependencies", "scopes", and * "classifiers" are added to the modelpath. This is the "modelPath" option of * MontiCore. */ @Parameter private List<File> modelPaths; /** * @return the value of the "modelPaths" configuration parameter. */ protected Set<File> getModelPaths() { ImmutableSet.Builder<File> modelPaths = ImmutableSet.builder(); // 1st: we take all modelpaths directly from the configuration if (this.modelPaths != null) { this.modelPaths.forEach(mP -> modelPaths.add(fromBasePath(mP))); } // 2nd: if specified we add any grammar directories (default) if (addGrammarDirectoriesToModelPath()) { for (File grammarFile : getGrammars()) { if (grammarFile.isDirectory()) { modelPaths.add(grammarFile); } } } // 3rd: if specified we add the project source directories (non default) if (addSourceDirectoriesToModelPath()) { getMavenProject().getCompileSourceRoots().forEach(csR -> modelPaths.add(new File(csR))); } // 4th: if specified we add the entire project "compile" classpath (non // default) if (addClassPathToModelPath()) { try { getMavenProject().getCompileClasspathElements().forEach( cpDir -> modelPaths.add(new File(cpDir))); } catch (DependencyResolutionRequiredException e) { Throwables.propagate(e); } } for (Artifact artifact : getMavenProject().getArtifacts()) { // 5th: we add any explicitly specified project dependencies if (getModelPathDependencies().contains( getArtifactDescription(artifact.getGroupId(), artifact.getArtifactId()))) { modelPaths.add(artifact.getFile()); } // 6th: we add any project dependencies matching the classifier/scope // filter (default: all "grammars", "grammar", and "symbols" dependencies // of any scope) // FIXME (minor): this does not work in reactor builds which do not invoke // the install phase (e.g., mvn dependency:analyze) else if (getClassifiers().contains(artifact.getClassifier()) && (getScopes().isEmpty() || getScopes().contains(artifact.getScope()))) { modelPaths.add(artifact.getFile()); } } return modelPaths.build(); } /** * A list of dependencies to be added to the model path. The dependencies must * be stated as "groupID:artifactID" and must be declared in the project's * dependencies. */ @Parameter private List<String> modelPathDependencies; /** * @return modelDependencies */ protected List<String> getModelPathDependencies() { return firstNonNull(this.modelPathDependencies, ImmutableList.<String> of()); } /** * Indicates whether any grammar directories should be added to the modelpath * (true by default). */ @Parameter(defaultValue = "true") private boolean addGrammarDirectoriesToModelPath = true; /** * @return the value of the "addGrammarDirectoriesToModelPath" configuration * parameter. */ protected boolean addGrammarDirectoriesToModelPath() { return this.addGrammarDirectoriesToModelPath; } /** * Indicates whether the project source directories should be added to the * modelpath (false by default). */ @Parameter(defaultValue = "false") private boolean addSourceDirectoriesToModelPath = false; /** * @return the value of the "addSourceDirectoriesToModelPath" configuration * parameter. */ protected boolean addSourceDirectoriesToModelPath() { return this.addSourceDirectoriesToModelPath; } /** * Indicates whether the (compile) classpath (entries) of the Maven project * should be added to the modelpath (false by default). */ @Parameter(defaultValue = "false") private boolean addClassPathToModelPath = false; /** * @return the value of the "addClassPathToModelPath" configuration parameter. */ protected boolean addClassPathToModelPath() { return this.addClassPathToModelPath; } /** * Indicates whether the output directory should be added to the modelpath * (true by default). */ @Parameter(defaultValue = "true") private boolean addOutputDirectoryToModelPath = false; /** * @return the value of the "addOutputDirectoryToModelPath" configuration * parameter. */ protected boolean addOutputDirectoryToModelPath() { return this.addOutputDirectoryToModelPath; } /** * The scopes to be considered for dependencies to add to the modelpath (any * scope by default). */ @Parameter private List<String> scopes = ImmutableList.of(); /** * @return the value of the "scopes" configuration parameter. */ protected List<String> getScopes() { return this.scopes; } /** * The classifiers to be considered for dependencies to add to the modelpath * (defaults to "grammars", "grammar", "symbols"). */ @Parameter(defaultValue = "grammars, grammar, symbols") private List<String> classifiers = ImmutableList.of(); /** * @return the value of the "classifiers" configuration parameter. */ protected List<String> getClassifiers() { return firstNonNull(this.classifiers, ImmutableList.<String> builder() .add("grammars") .add("grammar") .add("symbols") .build()); } /** * An optional alternative Groovy script to execute. This may either be an * absolute or relative path in the current project classpath. */ @Parameter private String script = null; /** * @return the value of the "script" configuration parameter. */ protected String getScript() { return this.script; } /** * A set of custom key value arguments to be passed to the Groovy script. The * value of an argument may be a space separated list of values. This has no * impact on the default script. It is solely passed to custom scripts * provided by the "script" parameter. */ @Parameter private Map<String, String> arguments = Collections.emptyMap(); /** * @return the value of the "arguments" configuration parameter. */ protected Map<String, String> getArguments() { return this.arguments; } /** * Switch to control the "force" configuration parameter to force generation * bypassing the incremental check (defaults to false). */ @Parameter(defaultValue = "false") private boolean force = false; /** * @return the value of the "force" configuration parameter. */ protected boolean getForce() { return this.force; } /** * Switch to skip the plugin execution. Defaults to false. */ @Parameter(defaultValue = "false") private boolean skip = false; /** * @return whether this plugin should be skipped. */ protected boolean skip() { return this.skip; } /** * @see org.apache.maven.plugin.Mojo#execute() */ @Override public final void execute() throws MojoExecutionException, MojoFailureException { if (skip()) { getLog().info("Plugin execution skipped per configuration."); return; } // create all necessary directories getOutputDirectory().mkdirs(); getSymbolTableDirectory().mkdirs(); // setup configuration Map<String, Iterable<String>> parameters = new HashMap<>(); parameters.put(GRAMMARS.toString(), toStringSet(getGrammars())); parameters.put(MODELPATH.toString(), toStringSet(getModelPaths())); parameters.put(HANDCODEDPATH.toString(), toStringSet(getHandcodedPaths())); parameters.put(TEMPLATEPATH.toString(), toStringSet(getTemplatePaths())); parameters.put(OUT.toString(), Arrays.asList(getOutputDirectory().getAbsolutePath())); parameters.put(OUTTOMODELPATH.toString(), Arrays.asList(Boolean.toString(addOutputDirectoryToModelPath()))); if (getForce()) { parameters.put(FORCE.toString(), new ArrayList<>()); } Configuration configuration = ConfigurationPropertiesMapContributor.fromSplitMap(parameters); // this temporary wrap in a MontiCoreConfiguration is only here to make the // check for .mc4 files easier MontiCoreConfiguration mcConf = MontiCoreConfiguration.withConfiguration(configuration); // we check if there are any ".mc4" files in the input parameter Iterator<Path> grammarPaths = mcConf.getGrammars().getResolvedPaths(); if (!grammarPaths.hasNext()) { getLog() .warn( "0xA1035 Skipping MontiCore as there are no \".mc4\" files in the input parameter. Check your \"grammars\" parameter configuration."); return; } // run the default script if (getScript() == null) { // run script new MontiCoreScript().run(configuration); } // or a provided custom script else { try { String script = null; ClassLoader l = getClass().getClassLoader(); URL r = l.getResource(getScript()); if (r == null) { r = fromBasePath(getScript()).toURI().toURL(); } Scanner scr = new Scanner(r.openStream()); script = scr.useDelimiter("\\A").next(); scr.close(); // adds any custom arguments additionally to the default ones getArguments() .forEach((key, value) -> parameters.put(key, Arrays.asList(value.split(" ")))); // this is a Configuration which has the generic arguments in addition configuration = ConfigurationPropertiesMapContributor.fromSplitMap(parameters); // if (getLog().isDebugEnabled()) { getLog().debug("Configuration content:"); configuration.getAllValues().forEach( (key, value) -> getLog().debug("Key: " + key + " -> " + value.toString())); // } new MontiCoreScript().run(script, configuration); } catch (IOException e) { throw new MojoFailureException("0xA4089 Failed to load the specified script.", e); } } // if everything went well we also need to add the generated output to the // Maven project compile roots getMavenProject().addCompileSourceRoot(getOutputDirectory().getPath()); getLog().debug("Adding compile source root: " + getOutputDirectory().getPath()); } /** * @return a new {@link File} that is absolutized based on the current Maven * project's base directory. */ protected File fromBasePath(File file) { return fromBasePath(file.getPath()); } /** * @return a new {@link File} that is absolutized based on the current Maven * project's base directory. */ protected File fromBasePath(String filePath) { File file = new File(filePath); return !file.isAbsolute() ? new File(getMavenProject().getBasedir(), filePath) : file; } /** * @param groupId * @param artifactId * @return groupId + ":" + artifactId */ protected String getArtifactDescription(String groupId, String artifactId) { return groupId.trim() + ":" + artifactId.trim(); } /** * @param files * @return a set of all the file names of the given collection of files */ protected Set<String> toStringSet(Collection<File> files) { return ImmutableSet.<String> builder() .addAll(Iterables.transform(files, file -> file.getPath())).build(); } }